﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using SuperMap.Data;
using SuperMap.Data.Processing;

namespace ArcMapCache2SupermapCache
{
    public class CacheWriter
    {
        public event CacheWritingEventHandler CacheWriting;
        protected virtual void OnCacheWriting(CacheWritingEventArgs e)
        {
            if (this.CacheWriting != null)
            {
                this.CacheWriting(this, e);
            }
        }

        public String CacheName
        {
            get;
            set;  
        }

        public TileFormat TileFormat
        {
            get;
            set;
        }

        public TileSize TileSize
        {
            get;
            set;
        }

        public String HashCode
        {
            get;
            set;
        }

        public Dictionary<Double, String> CacheScaleCaptions
        {
            get;
            private set;
        }

        public void SetLocalScaleCaptions(Double[] scales)
        {
            this.CacheScaleCaptions.Clear();
            foreach (Double scale in scales)
            {
                if (!this.CacheScaleCaptions.ContainsKey(scale))
                {
                    this.CacheScaleCaptions.Add(scale, ((Int32)(1 / scale)).ToString());
                }
            }
        }

        public void SetGlobalScaleCaptions(Int32[] levels)
        {
            this.CacheScaleCaptions.Clear();
            if (this.PrjCoordSys == null)
            {
                throw new ArgumentNullException("PrjCoordSys", "对象不能为空。");
            }
            if (this.PrjCoordSys.GeoCoordSys == null)
            {
                throw new ArgumentNullException("PrjCoordSys.GeoCoordSys", "对象不能为空。");
            }
            if (this.PrjCoordSys.GeoCoordSys.Type != GeoCoordSysType.Wgs1984)
            {
                throw new NotSupportedException("必须为WGS1984地理坐标系统。");
            }
            if (this.DPI <= 0)
            {
                throw new ArgumentOutOfRangeException("DPI", "DPI必须大于零。");
            }
            foreach (Int32 item in levels)
            {
                if (item < 0 || item > 100)
                {
                    throw new ArgumentOutOfRangeException("levels[]", "全球剖分级别值必须在 0 和 100 之间。");
                }

                Double scale = 1 / (6378137.0 * 10000 * Math.PI * this.DPI / (254 * ((Int32)this.TileSize) * Math.Pow(2, item)));
                if (!this.CacheScaleCaptions.ContainsKey(scale))
                {
                    this.CacheScaleCaptions.Add(scale, item.ToString());
                }
            }
        }

        public MapTilingMode CacheMode
        {
            get;
            set;
        }

        public PrjCoordSys PrjCoordSys
        {
            get;
            set;
        }

        public Double DPI
        {
            get;
            set;
        }

        public StorageType StorageType
        {
            get;
            set;
        }

        public Rectangle2D CacheBounds
        {
            get;
            set;
        }

        public Rectangle2D IndexBounds
        {
            get;
            set;
        }

        public String OutputFolder
        {
            get;
            set;
        }

        public String Provider
        {
            get;
            set;
        }

        public Boolean Transparent
        {
            get;
            set;
        }

        public CacheWriter()
        {
            this.CacheName = "Cache";
            this.TileFormat = TileFormat.JPG;
            this.TileSize = TileSize.Size256;
            this.HashCode = "";
            this.Provider = "";
            this.CacheScaleCaptions = new Dictionary<Double, String>();
            m_version = 5.1;
            this.PrjCoordSys = null;
            m_defaultColor = Color.White;
            this.Transparent = false;
            m_tileCompress = 100;
            m_fillMargin = true;
            this.StorageType = StorageType.Original;
            this.CacheMode = MapTilingMode.Local;
            m_hashCodeEnabled = false;
            this.DPI = 96.0;
            this.CacheBounds = new Rectangle2D();
            this.IndexBounds = new Rectangle2D();
            m_dataBounds = new Rectangle2D();
            m_clipRegion = new GeoRegion();
        }

        public Boolean Build()
        {
            try
            {
                String sciName = String.Format("{0}/{1}/{1}.sci", this.OutputFolder, this.CacheName);
                String infName = String.Format(
                    "{0}/{1}/{2}_{3}{4}/{2}_{3}{4}.inf",
                    this.OutputFolder,
                    this.CacheName,
                    ParseTileFormat(this.TileFormat),
                    (Int32)this.TileSize,
                    String.IsNullOrEmpty(this.HashCode) ? "" : "_" + this.HashCode);
                if (!ToConfigFile(sciName) || !ToConfigFile(infName))
                {
                    return false;
                }

                Double unitRatio = AdjustUnitsRatio();

                foreach (Double scale in this.CacheScaleCaptions.Keys)
                {
                    Double resolution = 1 / (scale * this.DPI / 254); // 单位：0.1mm/pixel
                    resolution = resolution / unitRatio; // 单位:投影单位/pixel

                    Double tileResolution = resolution * (Int32)this.TileSize;

                    Int32 startXIndex = (Int32)Math.Floor(((this.CacheBounds.Left - this.IndexBounds.Left) / tileResolution));
                    Int32 startYIndex = (Int32)Math.Floor(-((this.CacheBounds.Top - this.IndexBounds.Top) / tileResolution));
                    Int32 endXIndex = (Int32)Math.Floor(((this.CacheBounds.Right - this.IndexBounds.Left) / tileResolution));
                    Int32 endYIndex = (Int32)Math.Floor(-((this.CacheBounds.Bottom - this.IndexBounds.Top) / tileResolution));

                    for (Int32 y = startYIndex; y <= endYIndex; y++)
                    {
                        for (Int32 x = startXIndex; x <= endXIndex; x++)
                        {
                            Rectangle2D tileBounds = new Rectangle2D(
                                    this.IndexBounds.Left + x * tileResolution,
                                    this.IndexBounds.Top - (y + 1) * tileResolution,
                                    this.IndexBounds.Left + (x + 1) * tileResolution,
                                    this.IndexBounds.Top - y * tileResolution);
                            String tilePath = GetLocalTilePath(scale, y, x);

                            CacheTileParameter cacheTileParameter = new CacheTileParameter(
                                    scale, y, x, tileBounds, tilePath);
                            CompactTileParameter compactTileParameter = null; 
                            if (this.StorageType == StorageType.Compact)
                            {
                                compactTileParameter = new CompactTileParameter(
                                        y % 128, x % 128);
                            }
                            WriteTile(cacheTileParameter, compactTileParameter);
                        }
                    }
                }

                return true;
            }
            catch (Exception ex)
            {
                return false;
            }
        }

        protected virtual void WriteTile(CacheTileParameter cacheTileParameter, CompactTileParameter compactTileParameter)
        {
            OnCacheWriting(new CacheWritingEventArgs(cacheTileParameter, compactTileParameter));
        }

        public String GetGolbalTilePath(Int32 level, Int32 row, Int32 col)
        {
            String result = String.Empty;
            Double sacle = 0;
            foreach (Double key in this.CacheScaleCaptions.Keys)
            {
                if (this.CacheScaleCaptions[key] == level.ToString())
                {
                    sacle = key;
                }
            }

            if (sacle != 0)
            {
                result = GetLocalTilePath(sacle, row, col);
            }
            return result;
        }

        public String GetLocalTilePath(Double scale, Int32 row, Int32 col)
        {
            if (scale <= 0)
            {
                return "";
            }

            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append(this.OutputFolder);
            if (!String.IsNullOrEmpty(this.OutputFolder) && (!(this.OutputFolder.EndsWith("/") || this.OutputFolder.EndsWith("\\"))))
            {
                stringBuilder.Append("/");
            }
            stringBuilder.Append(this.CacheName);
            stringBuilder.Append("/");
            stringBuilder.Append(ParseTileFormat(this.TileFormat));
            stringBuilder.Append("_");
            stringBuilder.Append((Int32)this.TileSize);
            stringBuilder.Append("_");
            stringBuilder.Append(this.HashCode);
            stringBuilder.Append("/");
            if (this.CacheScaleCaptions.ContainsKey(scale))
            {
                stringBuilder.Append(this.CacheScaleCaptions[scale]);
            }
            else
            {
                stringBuilder.Append((Int32)(1 / scale));
            }
            stringBuilder.Append("/");
            Int32 tempvalue = row < 0 ? row / 128 - 1 : row / 128;
            stringBuilder.Append(tempvalue);
            stringBuilder.Append("/");
            tempvalue = col < 0 ? col / 128 - 1 : col / 128;
            stringBuilder.Append(tempvalue);
            if (this.StorageType == StorageType.Original)
            {
                stringBuilder.Append("/");
                stringBuilder.Append(row);
                stringBuilder.Append("x");
                stringBuilder.Append(col);
                stringBuilder.Append(GetTileExtension(this.TileFormat));
            }
            else
            {
                stringBuilder.Append(".cf");
            }
            return stringBuilder.ToString();
        }

        private String GetTileExtension(TileFormat tileFormat)
        {
            String result = "";
            switch (tileFormat)
            {
                case TileFormat.GIF:
                    {
                        result = ".gif";
                    }
                    break;
                case TileFormat.JPG:
                case TileFormat.JPG_PNG:
                    {
                        result = ".jpg";
                    }
                    break;
                case TileFormat.PNG:
                case TileFormat.PNG8:
                    {
                        result = ".png";
                    }
                    break;
                default:
                    break;
            }
            return result;
        }

        private Double AdjustUnitsRatio()
        {
            Double unitRatio = 0; // 单位：0.1mm/投影单位
            if (this.PrjCoordSys.CoordUnit == Unit.Degree)
            {
                unitRatio = this.PrjCoordSys.GeoCoordSys.GeoDatum.GeoSpheroid.Axis * ((Int32)this.PrjCoordSys.CoordUnit - 1000000000) * 10000 * Math.PI / (180.0 * 1745329);
            }
            else
            {
                unitRatio = (Double)this.PrjCoordSys.CoordUnit;
            }
            return unitRatio;
        }

        public Boolean ToConfigFile(String fileName)
        {
            m_dataBounds = this.CacheBounds;
            if (this.IndexBounds.IsEmpty)
            {
                //this.IndexBounds = this.CacheBounds;
            }
            if (this.CacheMode == MapTilingMode.Global)
            {
                this.IndexBounds = new Rectangle2D(-180, -90, 180, 90);
            }

            return !String.IsNullOrEmpty(this.CacheName) &&
                this.PrjCoordSys != null &&
                this.DPI > 0 &&
                !this.CacheBounds.IsEmpty &&
                !String.IsNullOrEmpty(this.OutputFolder) &&
                ToConfig(fileName);
        }

        private Boolean ToConfig(String fileName)
        {
            Boolean result = false;

            try
            {
                StringBuilder cacheXml = new StringBuilder();
                cacheXml.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                cacheXml.AppendLine();

                cacheXml.Append("<SuperMapCache>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:Version>");
                cacheXml.Append(String.Format("{0}.0", (Int32)(m_version * 10) / 10));
                cacheXml.Append("</sml:Version>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:MinorVersion>");
                cacheXml.Append((Int32)(m_version * 10) % 10);
                cacheXml.Append("</sml:MinorVersion>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:VersionRequired>");
                cacheXml.Append(String.Format("{0}.0", (Int32)(m_version * 10) / 10));
                cacheXml.Append("</sml:VersionRequired>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:CacheName>");
                cacheXml.Append(this.CacheName);
                cacheXml.Append("</sml:CacheName>");
                cacheXml.AppendLine();

                if (String.IsNullOrEmpty(this.Provider))
                {
                    cacheXml.Append("<sml:Provider/>");
                }
                else
                {
                    cacheXml.Append("<sml:Provider>");
                    cacheXml.Append(this.Provider);
                    cacheXml.Append("</sml:Provider>");
                }
                cacheXml.AppendLine();

                cacheXml.Append("<sml:MapName>");
                cacheXml.Append(this.CacheName);
                cacheXml.Append("</sml:MapName>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:HashCode>");
                cacheXml.Append(this.HashCode);
                cacheXml.Append("</sml:HashCode>");
                cacheXml.AppendLine();

                cacheXml.Append(this.PrjCoordSys.ToXML(20031211));// TODO:20031211

                cacheXml.Append("<sml:ImageType>");
                cacheXml.Append(this.TileFormat);
                cacheXml.Append("</sml:ImageType>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:DefaultColor>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Red>");
                cacheXml.Append(String.Format("0x{0:X}", m_defaultColor.R));
                cacheXml.Append("</sml:Red>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Green>");
                cacheXml.Append(String.Format("0x{0:X}", m_defaultColor.G));
                cacheXml.Append("</sml:Green>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Blue>");
                cacheXml.Append(String.Format("0x{0:X}", m_defaultColor.B));
                cacheXml.Append("</sml:Blue>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Alpha>");
                cacheXml.Append(String.Format("0x{0:X}", m_defaultColor.A));
                cacheXml.Append("</sml:Alpha>");
                cacheXml.AppendLine();
                cacheXml.Append("</sml:DefaultColor>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:Transparent>");
                cacheXml.Append(this.Transparent.ToString().ToUpper());
                cacheXml.Append("</sml:Transparent>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:ImageCompress>");
                cacheXml.Append(m_tileCompress);
                cacheXml.Append("</sml:ImageCompress>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:FillMargin>");
                cacheXml.Append(m_fillMargin.ToString().ToUpper());
                cacheXml.Append("</sml:FillMargin>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:StorageType>");
                cacheXml.Append(this.StorageType);
                cacheXml.Append("</sml:StorageType>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:TileType>");
                cacheXml.Append(this.CacheMode.ToString().ToUpper());
                cacheXml.Append("</sml:TileType>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:HashCodeEnabled>");
                cacheXml.Append(m_hashCodeEnabled.ToString().ToUpper());
                cacheXml.Append("</sml:HashCodeEnabled>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:ImageSize>");
                cacheXml.Append((Int32)this.TileSize);
                cacheXml.Append("</sml:ImageSize>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:Bounds>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Left>");
                cacheXml.Append(this.CacheBounds.Left);
                cacheXml.Append("</sml:Left>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Top>");
                cacheXml.Append(this.CacheBounds.Top);
                cacheXml.Append("</sml:Top>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Right>");
                cacheXml.Append(this.CacheBounds.Right);
                cacheXml.Append("</sml:Right>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Bottom>");
                cacheXml.Append(this.CacheBounds.Bottom);
                cacheXml.Append("</sml:Bottom>");
                cacheXml.AppendLine();
                cacheXml.Append("</sml:Bounds>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:CoordRatio>");
                cacheXml.Append(this.DPI / 25.4);
                cacheXml.Append("</sml:CoordRatio>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:IndexBounds>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Left>");
                cacheXml.Append(this.IndexBounds.Left);
                cacheXml.Append("</sml:Left>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Top>");
                cacheXml.Append(this.IndexBounds.Top);
                cacheXml.Append("</sml:Top>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Right>");
                cacheXml.Append(this.IndexBounds.Right);
                cacheXml.Append("</sml:Right>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Bottom>");
                cacheXml.Append(this.IndexBounds.Bottom);
                cacheXml.Append("</sml:Bottom>");
                cacheXml.AppendLine();
                cacheXml.Append("</sml:IndexBounds>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:MapBounds>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Left>");
                cacheXml.Append(m_dataBounds.Left);
                cacheXml.Append("</sml:Left>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Top>");
                cacheXml.Append(m_dataBounds.Top);
                cacheXml.Append("</sml:Top>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Right>");
                cacheXml.Append(m_dataBounds.Right);
                cacheXml.Append("</sml:Right>");
                cacheXml.AppendLine();
                cacheXml.Append("<sml:Bottom>");
                cacheXml.Append(m_dataBounds.Bottom);
                cacheXml.Append("</sml:Bottom>");
                cacheXml.AppendLine();
                cacheXml.Append("</sml:MapBounds>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:Scales>");
                cacheXml.AppendLine();
                foreach (Double key in this.CacheScaleCaptions.Keys)
                {
                    cacheXml.Append("<sml:Scale>");
                    cacheXml.AppendLine();
                    cacheXml.Append("<sml:Value>");
                    cacheXml.Append(key);
                    cacheXml.Append("</sml:Value>");
                    cacheXml.AppendLine();
                    cacheXml.Append("<sml:Caption>");
                    cacheXml.Append(this.CacheScaleCaptions[key]);
                    cacheXml.Append("</sml:Caption>");
                    cacheXml.AppendLine();
                    cacheXml.Append("</sml:Scale>");
                    cacheXml.AppendLine();
                }
                cacheXml.Append("</sml:Scales>");
                cacheXml.AppendLine();

                cacheXml.Append("<sml:ClipRegion/>");
                cacheXml.AppendLine();

                cacheXml.Append("</SuperMapCache>");


                if (!Directory.Exists(Path.GetDirectoryName(fileName)))
                {
                    Directory.CreateDirectory(Path.GetDirectoryName(fileName));
                }
                using (StreamWriter sw = new StreamWriter(fileName, false, Encoding.UTF8))
                {
                    sw.Write(cacheXml);
                }
                result = true;
            }
            catch
            {
                result = false;
            }
            return result;
        }

        private String ParseTileFormat(TileFormat tileFormat)
        {
            String result = "";
            switch (tileFormat)
            {
                case TileFormat.GIF:
                    {
                        result = "G";
                    }
                    break;
                case TileFormat.JPG:
                    {
                        result = "J";
                    }
                    break;
                case TileFormat.JPG_PNG:
                    {
                        result = "JP";
                    }
                    break;
                case TileFormat.PNG:
                    {
                        result = "P";
                    }
                    break;
                case TileFormat.PNG8:
                    {
                        result = "P8";
                    }
                    break;
                default:
                    break;
            }
            return result;
        }


        #region 私有成员

        private Double m_version;
        private Color m_defaultColor;
        private Int32 m_tileCompress;

        public Int32 TileCompress
        {
            get { return m_tileCompress; }
            set { m_tileCompress = value; }
        }
        private Boolean m_fillMargin;
        private Boolean m_hashCodeEnabled;
        private Rectangle2D m_dataBounds;
        private GeoRegion m_clipRegion;

        #endregion
    }

    public delegate void CacheWritingEventHandler(Object sender, CacheWritingEventArgs e);

    public class CacheWritingEventArgs : EventArgs
    {
        public CacheTileParameter CacheTileParameter
        {
            get;
            private set;
        }

        public CompactTileParameter CompactTileParameter
        {
            get;
            private set;
        }

        public CacheWritingEventArgs(CacheTileParameter cacheTile, CompactTileParameter compactTile)
        {
            this.CacheTileParameter = cacheTile;
            this.CompactTileParameter = compactTile;
        }
    }

    public class CacheTileParameter
    {
        public Double TileScale
        {
            get;
            private set;
        }

        public Int32 TileRow
        {
            get;
            private set;
        }

        public Int32 TileCol
        {
            get;
            private set;
        }

        public Rectangle2D TileBounds
        {
            get;
            private set;
        }

        public String TilePath
        {
            get;
            private set;
        }

        public CacheTileParameter(Double scale, Int32 row, Int32 col, Rectangle2D bounds, String path)
        {
            this.TileScale = scale;
            this.TileRow = row;
            this.TileCol = col;
            this.TileBounds = bounds;
            this.TilePath = path;
        }

    }

    public class CompactTileParameter
    {
        public Int32 Row
        {
            get;
            private set;
        }

        public Int32 Col
        {
            get;
            private set;
        }

        public Int32 RowNumForCreate
        {
            get;
            private set;
        }

        public Int32 ColNumForCreate
        {
            get;
            private set;
        }

        public Byte[] TileHeader
        {
            get;
            private set;
        }

        public CompactTileParameter(Int32 row, Int32 col)
        {
            this.Row = row;
            this.Col = col;
            this.RowNumForCreate = 128;
            this.ColNumForCreate = 128;
            this.TileHeader = new Byte[12];
            this.TileHeader[0] = 0;
        }
    }
}
